# -*- coding: utf-8 -*-
"""
本代码用于读取笔迹坐标，并进行相关处理，以适应DBRHD数据集的格式，方便后续识别操作。

笔迹获取自draw.html返回的数据，是一行二维坐标点的序列，以逗号间隔每个数值。

关于DBRHD数据集的描述，在pendigits.names中以及uci数据集网站有官方的说明，这里摘取和本程序相关的部分：
数据集中一个数字的原始字迹是以一定采样率采样的，也就是当实验者写字时，每隔100ms采集一次笔的坐标。为了
计算笔迹间的欧式距离，需要把笔迹resample成维度相等的向量。他们的做法是将笔迹按弧长等距分为8个坐标点，也
就是16个数值，按顺序排列，数值范围在0到100之间，这就是pendigits.tra和pendigits.tes中数据的组织格式。

本程序对采集到的笔迹采取同样的操作，步骤与下方imooc链接相似：
https://www.imooc.com/learn/131
1. resample， 计算字迹的弧长，该长度处以7得到两点之间的距离，以该距离等距取8个坐标点。
2. scale， 用获得的8个坐标点计算字迹的box（一张大图片中字迹所占的方框），将字迹缩放成统一大小，即使box伸缩为100x100的方框。
3. center, 将缩放后的坐标点居中。//该操作隐性地包含在PreProcessor.coordination_translate方法中。
4. condition_translate，html和PIL等计算机图形领域使用的二维坐标系原点在屏幕左上角，y轴向下，而数据集中轨迹的坐标系
    原点在左下角，y轴向上，所以要和数据集中轨迹比对时，首先要变换坐标系。
5. rotate， (可能不做此功能)用伸缩后的坐标计算中点，将字迹第一点和中点相连，旋转至最近的角度，详见imooc链接。

2019-2-13
"""

import numpy as np
import re
import logging

logging.basicConfig(level=logging.DEBUG)

RESAMPLE_NUM = 8  # number of resampled points
NORMALIZED_SIZE = (100, 100)  # pixels, the max value of output trajectory


class BoxError(Exception):
    def __init__(self, *args, **kw):
        text = "Cannot find the a box of the given points."
        super(BoxError, self).__init__(text, *args, **kw)


def get_arc_length(points):
    """Calculate the arc length of the points, by adding the linear distance between the ordered points.
    Return the arc length and the distances between the ordered points pair"""
    arc_length = 0
    # The distances between each point pairs might be used other place(such as resample),
    # so we return it to avoid repeat calculation.
    distances = []

    x0, y0 = points[0]
    for p in points:
        x, y = p
        dis = np.sqrt((x-x0)**2 + (y-y0)**2)
        arc_length += dis
        distances.append(dis)
        x0, y0 = x, y

    distances.pop(0)  # the first distance is useless
    return arc_length, np.asarray(distances)


class PreProcessor:
    def __init__(self, raw_data=None):
        self.raw = raw_data  # <type str>
        self.raw_points = self.load(raw_data)
        self._output_size = NORMALIZED_SIZE

    def load(self, raw_data):
        """Load the raw data, return a 2 dimensional numpy array containing the points.
        If no parameter is passed, it will return None.
        eg:
        >>>raw_data = '20,58, 54,34, 66,53'
        >>>points = self.load(raw_data)
        >>>print(points)
        [[20,58],
         [54,34],
         [66,53]]
        """
        if raw_data is None:
            return None

        raw_data = re.split(',', raw_data)

        num = len(raw_data)
        if num % 2 != 0:
            # to make the num of coordinate is even
            raw_data.pop(-1)

        # logging.debug('raw data @ load(): %s ' % str(raw_data))
        # convert str into float, because <str> '1.0' cannot be straightly converted into <int> 1.
        rslt = np.asarray(raw_data, dtype=float)
        rslt = np.asarray(rslt, dtype=int)  # convert <float> into <int>
        rslt = rslt.reshape([int(num/2), 2])  # reshape to array of points

        self.raw_points = rslt.copy()
        return rslt

    def resample(self, points):
        num = RESAMPLE_NUM
        arc_len, distances = get_arc_length(points)
        avg_len = arc_len / (num - 1)

        i = 0
        length = 0
        x0, y0 = points[0]  # initial point
        resampled_points = [(x0, y0)]  # the start point and end point should be resampled
        while True:
            if i >= len(distances):
                break

            if length + distances[i] >= avg_len:
                diff = avg_len - length  # the diff added which makes the length match the avg_len
                x1, y1 = points[i+1]
                x, y = x0 + (x1-x0)*(diff/distances[i]), y0 + (y1-y0)*(diff/distances[i])  # calculate by proportion

                resampled_points.append((x, y))

                # Without this wired expression, the following variable will be local variable
                # So I have to reset or update the variable of higher scope in this way.
                length -= length

                # Special cases when we write "4" or "5", we always write half of the digit at first,
                # take the pen up, move to other place, then take the pen down to write the rest of the digit.
                # In Such cases, there will be two points covering a long distance, between which it might be possible
                # to resample more than one point. So here we need to handle such cases.
                rest = np.sqrt((x1-x)**2 + (y1-y)**2)
                if rest < avg_len:
                    i += 1
                    length += rest  # add the rest of distance
                    # update the points
                    x, y = points[i]

                x0 += x - x0
                y0 += y - y0
            else:
                length += distances[i]
                i += 1
                x, y = points[i]
                x0 += x - x0
                y0 += y - y0

        if len(resampled_points) < num:
            resampled_points.append(points[-1])

        return np.asarray(resampled_points, dtype=int)

    def scale(self, points):
        new_size = NORMALIZED_SIZE
        left, top, right, bottom = self.get_box(points)
        ratio_x = new_size[0] / (right - left)
        ratio_y = new_size[1] / (bottom - top)

        points = min(ratio_x, ratio_y) * points
        points = np.asarray(points, dtype=int)  # convert to int

        return points

    def center(self, points, box=None):
        if box is None:
            left, top, right, bottom = self.get_box(points)
        else:
            left, top, right, bottom = box

        # the bias to the center
        bias = self._output_size[0]/2 - (left + right)/2, self._output_size[1]/2 - (top + bottom)/2
        points[:, 0] += int(bias[0])
        points[:, 1] += int(bias[1])

        return points

    def get_box(self, points):
        """Get the box of (left, top, right, bottom) of the given points."""
        points = np.asarray(points, dtype=int)
        left = points[:, 0].min()
        right = points[:, 0].max()
        top = points[:, 1].min()
        bottom = points[:, 1].max()

        try:
            box = left, top, right, bottom
        except NameError:
            print("This shouldn't have happened.")
            raise BoxError

        return box

    def coordination_translate(self, points):
        """Translate the coordination used by screen and html5 canvas into
        the coordination used by dataset."""
        left, top, right, bottom = self.get_box(points)
        height = bottom - top

        points[:, 1] = -points[:, 1]
        points[:, 1] += height

        return points

    def export(self, if_reshape=True):
        """Export the points that can be used for recognition."""
        points = self.resample(self.raw_points)
        points = self.scale(points)
        # logging.debug('scaled:\n %s' % str(points))
        points = self.center(points)
        points = self.coordination_translate(points)
        if if_reshape:
            points = points.reshape([1, 2*RESAMPLE_NUM])  # format for sklearn knn classifier

        return points


if __name__ == '__main__':
    print('begin test...')
    print()

    with open('sample.txt') as f:
        text = f.readline()
        f.close()

    p = PreProcessor(text)
    points = p.load(text)
    print(points, end=', ')
    print(len(points))
    print(get_arc_length(points), end=', ')
    print(len(get_arc_length(points)[1]))

    # resample
    new_points = p.resample(points)
    print('resampled points:%s\n' % str(new_points), end=', ')
    print(len(new_points))

    # get box
    box = p.get_box(new_points)
    print("box:%s" % str(box))

    # scale
    scaled_points = p.scale(new_points)
    print("scaled: %s" % str(scaled_points))

    # center
    centered_points = p.center(scaled_points)

    # coordination translation
    fliped_points = p.coordination_translate(centered_points.copy())
    print('fliped point:%s' % str(fliped_points))

    import pygame
    pygame.init()
    scr = pygame.display.set_mode([600, 600])
    for i in range(len(points) - 1):
        pygame.draw.circle(scr, [255, 0, 0], points[i], 5)
        pygame.draw.line(scr, [0, 255, 0], points[i], points[i+1], 2)
        pygame.display.flip()
    for i in range(len(new_points)):
        pygame.draw.circle(scr, [255]*3, new_points[i], 5)
        # pygame.time.delay(500)
        pygame.display.flip()

    left, top, right, bottom = box
    pygame.draw.rect(scr, [255]*3, (left, top, right-left, bottom-top), 2)
    pygame.draw.circle(scr, [255, 0, 255], (left, top), 5)
    pygame.draw.circle(scr, [255, 0, 255], (right, bottom), 5)

    for i in range(len(scaled_points)-1):
        pygame.draw.line(scr, [0, 255, 0], scaled_points[i], scaled_points[i+1], 2)
        pygame.draw.circle(scr, [255]*3, scaled_points[i], 5)
    pygame.draw.circle(scr, [255]*3, scaled_points[-1], 5)

    for i in range(len(centered_points) -1):
        pygame.draw.line(scr, [255]*3, centered_points[i], centered_points[i+1], 2)

    for i in range(len(fliped_points)-1):
        x1, y1 = fliped_points[i]
        x2, y2 = fliped_points[i+1]
        # y1 += 100
        # y2 += 100
        print(x1, y1)
        pygame.draw.line(scr, [255, 255, 0], (x1, y1), (x2, y2), 2)

    pygame.display.flip()
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                quit()

